package com.sk.ccflower.lib.util;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;

import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sk.ccflower.lib.util.reflect.AffectEquality;
import com.sk.ccflower.lib.util.reflect.AnnotationPropertyFilter;
import com.sk.ccflower.lib.util.reflect.NameArrayPropertyFilter;
import com.sk.ccflower.lib.util.reflect.NoAnnotatedTargetException;
import com.sk.ccflower.lib.util.reflect.PropertyFilter;

/**
 * <p>Reflection을 이용해서  {@link Object#equals(Object)}  메소드를 재정의하거나, 두개의 객체를 비교할때 사용할 수 있는 유틸리티 메소드 모음.</p>
 * @author  dagui
 * @see  AffectEquality
 */
public class EqualityUtil {

	private static final Log log = LogFactory.getLog(EqualityUtil.class);
	
	private static final ThreadLocal<Boolean> debug = new ThreadLocal<Boolean>();
	
	public static boolean isDebug() {
		Boolean debug = EqualityUtil.debug.get();
		if (debug == null) {
			debug = Boolean.FALSE;
			EqualityUtil.debug.set(debug);
		}
		return debug.booleanValue();
	}

	public static boolean setDebug(boolean debug) {
		boolean old = isDebug();
		EqualityUtil.debug.set((debug)? Boolean.TRUE: Boolean.FALSE);
		return old;
	}

	/**
	 * <p>객체를 가능한 가장 적합한 방법으로 동일한지 비교한다.</p>
	 * <p>이 메소드는 다음과 같은 순서로 동작한다.</p> 
	 * <ol>
	 * 	<li>기본적인 검사(==, 같은 자료형인지 등등)를 통해서 동일성을 검사한다.</li>
	 * 	<li>객체가 배열인 경우 각 원소들을 다시 재귀호출로 비교한다.</li>
	 * 	<li>{@link Object#equals(Object)}를 재정의한 equals()메소드가 있는 경우 그 재정된 메소드를 사용하여 비교한다.</li>
	 * 	<li>{@link #annotationEquals(Object, Object)}를 이용해서 비교한다.</li>
	 * 	<li>{@link #propertyEquals(Object, Object)}를 이용해서 비교한다.</li>
	 * </ol>
	 * <p><strong>주의!</strong> 이메소드를 equals()를 구현하는 용도로 사용하지 말것
	 * ({@link StackOverflowError}가 발생할것임).
	 * 그런 용도로는 {@link #autoEquals(Object,Object)}를 사용할것</p>
	 * @param o1 비교 대상 객체1
	 * @param o2 비교 대상 객체2
	 * @return 문자열
	 */
	public static boolean equals(Object o1, Object o2) {
		if (o1 == o2)
			return true;
		else if ((o1 == null && o2 != null) ||
				(o1 != null && o2 == null))
			return false;
		else if (o1.getClass() != o2.getClass())
			return false;

		Class<?> clazz = o1.getClass();

		if (clazz.isArray()) {
			if (Array.getLength(o1) != Array.getLength(o1))
				return false;

			for (int i = 0; i < Array.getLength(o1); i++) {
				Object v1 = Array.get(o1, i);
				Object v2 = Array.get(o2, i);
				if (!equals(v1, v2)) {
					return false;
				}
			}
			return true;
		}
		else {
			if (hasOverridedEquals(o1)) {
				return o1.equals(o2);
			}
			else {
				try {
					return annotationEquals(o1, o2);
				}
				catch (NoAnnotatedTargetException e) {
					return propertyEquals(o1, o2);
				}
			}
		}
	}

	/**
	 * <p>{@link #annotationEquals(Object, Object)}를 먼저 시도하고,
	 * 실패하면 {@link #propertyEquals(Object, Object)} 를 사용하여 비교한다.</p>
	 * <p>이 메소드는 {@link Object#equals(Object)}를 간단하게 재정의하는 용도로 사용하면 된다.</p>
	 * @param o1 비교 대상 객체
	 * @param o2 비교 대상 객체
	 * @return 동일 값 여부
	 */
	public static boolean autoEquals(Object o1, Object o2) {
		if (o1 == null && o2 == null)
			return true;
		else if (o1 == null || o2 == null)
			return false;
		else if (o1 == o2)
			return true;
		else if (o1.getClass() != o2.getClass())
			return false;

		try {
			return annotationEquals(o1, o2);
		}
		catch (NoAnnotatedTargetException e) {
			return propertyEquals(o1, o2);
		}
	}
	
	/**
	 * <p>Alias of {@link #propertyEquals(Object, Object, String[])}.</p>
	 * @see #propertyEquals(Object, Object, String[])
	 */
	public static boolean equals(Object o1, Object o2, String[] propertyNames) {
		return propertyEquals(o1, o2, propertyNames);
	}

	/**
	 * 날짜/시간 비교시 지정한 필드 미만의 단위는 무시하고
	 * 그 이상의 필드까지만 유효한 값으로 비교한다.
	 * @param d1 비교 대상 날짜
	 * @param d2 비교 대상 날짜
	 * @param field 날짜/시간 필드
	 * @return 동일값 여부
	 */
	public static boolean dateEquals(Date d1, Date d2, int field) {
		if (d1 == null && d2 == null)
			return true;
		else if (d1 == null || d2 == null)
			return false;
		else if (d1 == d2 || d1.getTime() == d2.getTime())
			return true;
		
		Date t1 = DateUtils.truncate(d1, field);
		Date t2 = DateUtils.truncate(d2, field);
		return (t1.getTime() == t2.getTime());
	}

	/**
	 * <p>공개된 프로퍼티값을 이용해서 동일 객체 여부를 비교한다.</p>
	 * <p>이 메소드는 Commons-lang 의 {@link org.apache.commons.lang.builder.EqualsBuilder#reflectionEquals(Object,Object)}가
	 * 필드 기반으로 동작하는 관계로 {@link SecurityException}을 유발하기 때문에 보다 더 간편한고
	 * 쉽게 사용할 수는 없을까 고민하다가 만들게 되었다.</p>
	 * @see org.apache.commons.lang.builder.EqualsBuilder#reflectionEquals(Object,Object)
	 * @param o1 비교 대상 객체
	 * @param o2 비교 대상 객체
	 * @param filter 비교할 프로퍼티를 구분하는 필터
	 * @return 동일 값 여부
	 */
	public static boolean propertyEquals(Object o1, Object o2, PropertyFilter filter) {
		if (o1 == null && o2 == null)
			return true;
		else if (o1 == null || o2 == null)
			return false;
		else if (o1 == o2)
			return true;
		else if (o1.getClass() != o2.getClass())
			return false;

		PropertyDescriptor[] pds = ReflectUtil.getReadableProperties(o1.getClass(), filter);
		return propertyEquals(o1, o2, pds);
	}

	/**
	 * <p>공개된 프로퍼티값을 이용해서 동일 객체 여부를 비교한다.</p>
	 * @see #propertyEquals(Object, Object, PropertyFilter)
	 * @param o1 비교 대상 객체
	 * @param o2 비교 대상 객체
	 * @return 동일 값 여부
	 */
	public static boolean propertyEquals(Object o1, Object o2) {
		return propertyEquals(o1, o2, (PropertyFilter) null);
	}

	/**
	 * <p>공개된 프로퍼티값을 이용해서 동일 객체 여부를 비교한다.</p>
	 * @see #propertyEquals(Object, Object, PropertyFilter)
	 * @param o1 비교 대상 객체
	 * @param o2 비교 대상 객체
	 * @param propertyNames 비교할 프로퍼티명 목록
	 * @return 동일 값 여부
	 */
	public static boolean propertyEquals(Object o1, Object o2,
			String[] propertyNames) {
		return propertyEquals(o1, o2,
				new NameArrayPropertyFilter(propertyNames));
	}
	
	/**
	 */
	private static final PropertyFilter ANNOTATION_FILTER = new AnnotationPropertyFilter(
			AffectEquality.class, AnnotationPropertyFilter.METHOD_GETTER);

	/**
	 * <p>{@link AffectEquality} 어노테이션이 지정된 프로퍼티값을 이용해서 동일 객체 여부를 비교한다.</p>
	 * <p>{@link AffectEquality}은 getter 메소드에 지정해야 한다.</p>
	 * @see #propertyEquals(Object, Object, PropertyFilter)
	 * @param o1 비교 대상 객체
	 * @param o2 비교 대상 객체
	 * @return 동일 값 여부
	 * @throws NoAnnotatedTargetException {@link AffectEquality}를 지정한 메소드가 하나도 없을 경우 발생
	 */
	public static boolean annotationEquals(Object o1, Object o2) {
		if (o1 == null && o2 == null)
			return true;
		else if (o1 == null || o2 == null)
			return false;
		else if (o1 == o2)
			return true;
		else if (o1.getClass() != o2.getClass())
			return false;

		PropertyDescriptor[] pds = ReflectUtil.getReadableProperties(o1.getClass(), ANNOTATION_FILTER);
		if (pds == null || pds.length == 0)
			throw new NoAnnotatedTargetException("Class " + o1.getClass().getName() + " has no AffectEquality annotated method.");
		return propertyEquals(o1, o2, pds);
	}

	private static final Object[] NULL_PARAM = new Object[] {};

	private static boolean propertyEquals(Object o1, Object o2, PropertyDescriptor[] pds) {

		for (PropertyDescriptor pd: pds) {
			Method getter = pd.getReadMethod();
			if (log.isDebugEnabled()) {
				log.debug("test equality for property " + pd.getName());
			}

			// 값을 조회
			Object v1 = null, v2 = null;
			try {
				v1 = getter.invoke(o1, NULL_PARAM);
				v2 = getter.invoke(o2, NULL_PARAM);
			}
			catch (Exception e) {
				if (isDebug() && log.isWarnEnabled()) {
					log.warn("error while getting value of " + pd.getName() + ": " + e.getMessage());
					log.warn("error while getting value of " + pd.getName() + ": " + v1 + ", " + v2);
					e.printStackTrace();
				}
				return false;
			}
			
			if (v1 != v2 && (v1 == null || v2 == null || !equals(v1, v2))) {
				if (isDebug() && log.isWarnEnabled()) {
					log.warn(pd.getName() + " is different: " + v1 + ", " + v2);
				}
				return false;
			}
		}
		return true;
	}
	
	/**
	 * <p>{@link Object#equals(Object)}가 아닌 재정의된 equals() 메소드를 가지고 있는지 여부를 확인.</p>
	 */
	public static boolean hasOverridedEquals(Object o) {
		return (o == null)? false: hasOverridedEquals(o.getClass());
	}
	
	/**
	 * <p>{@link Object#equals(Object)}가 아닌 재정의된 equals() 메소드를 가지고 있는지 여부를 확인.</p>
	 */
	public static boolean hasOverridedEquals(Class<?> clazz) {
		Method[] methods = clazz.getMethods();
		for (Method method: methods) {
			if (method.getName().equals("equals")
					&& Modifier.isPublic(method.getModifiers())
					&& !Modifier.isStatic(method.getModifiers())
					&& method.getReturnType() == Boolean.TYPE
					&& method.getParameterTypes().length == 1
					&& method.getParameterTypes()[0] == Object.class) {
				// found equals();
				return (method.getDeclaringClass() != Object.class);
			}
		}
		return false;
	}

}
